Conversation
…d/rotation scaling Ships now derive their speed, acceleration, and rotation from a power-to-mass ratio based on sail composition. Wool blocks (3 pts) and banners (7 pts) provide sail power; every ship gets 2 free base points. The ratio linearly interpolates between absolute floors (1 block/sec speed, 30s/revolution rotation) and caps (1.5x default stats), with ratio 0.7 mapping to current defaults. Sails cap at ratio 0.8 — engines (not yet implemented) will be needed to push past it. Airship vertical speed scales with density magnitude rather than the horizontal ratio. Also adds ship_engine custom item definition (8 copper + blast furnace recipe) and updates the ship info display to show wool/banner counts, power ratio, and effective speed percentage.
Engine system: tagged blast furnace crafted from 8 copper + blast furnace. BlockPlaceEvent listener transfers PDC tag to TileState, vanilla smelting suppressed on engine blocks. Scanner and detection count engines via PDC check. Engine power (30 pts/engine) wired into the ratio calculation for both horizontal and airship vertical stats. Ship info display and detection chat messages now show engine count and engine-adjusted power ratio. Also extracts hardcoded help book content from ShipWheelMenu into HelpBookContent.java + help_book.yml, loaded once at plugin startup.
Adds the full engine fuel system on top of the engine detection foundation. Engines now require fuel to contribute power to the ship's ratio. Includes: - EngineMenuGUI: 3 fuel slots per engine, opened by right-clicking the engine block on an assembled ship. Validates fuel-only items, saves on close. - Fuel consumption: burns 1 tick per game tick while W held. Auto-consumes next fuel item when current burns out. Recomputes effective stats on change. - Smoke particles: CAMPFIRE_SIGNAL_SMOKE at fueled engine positions every 5 ticks while ship has a driver. - Fuel state persistence: per-engine fuel slots and burn ticks serialized to ship_wheels.yml via Base64-encoded ItemStack bytes. - Engine block indices and local positions tracked in ShipModel for click detection and particle spawning. - ShipInstance now holds a wheelData reference (set during assembly) for fuel state access in the physics loop. Note: DisplayShip.java and config.yml include unrelated changes from a concurrent Captain's Manual fix branch (shapeless recipes, help lore).
- Reload help book content on /blockships reload - Support shapeless recipes in ItemUtil.registerItemRecipe() - Replace verbose help lore (full book dump) with terse controls summary - Remove unused getHelpSections() from ShipWheelMenu - Fix speed percentage display: divide by sail cap (0.8) not default (0.7) so 100% means "max sails" rather than "default speed" - Bump floor-acceleration from 0.005 to 0.015 (less sluggish minimum)
…ne glint - Simplify ship info book hover to show only speed % (detailed breakdown moved to a new Ship Stats banner item at slot 20) - Speed % now uses sail cap (0.8) as 100% baseline instead of default ratio (0.7) — over 100% means engines are contributing - Stats banner shows wool/banner/engine counts, sail power with "capped at 80%" indicator when applicable, mass, power ratio, and speed % - Add config-driven enchantment glint support to CustomItem (enchant-glint field); ship_engine gets glint by default - Fix floor acceleration default mismatch (config says 0.015, code fallback said 0.005)
Previously only ship_wheel and ship kits were giveable. Now supports: - captains_manual (written help book) - any custom-items entry (ship_engine, balloon, etc.) Also adds tab completion for all giveable items and extracts item listing into shared helpers to avoid duplication.
New config keys added in plugin updates (like ship_engine, stats section) were invisible to existing servers because Bukkit's saveDefaultConfig() never overwrites an existing config.yml. Users had to manually delete their config to get new entries. migrateConfig() now runs at startup: loads the jar's bundled config, walks all leaf keys, and adds any that are missing from the user's config. Never overwrites existing values — customizations are preserved. Controlled by `auto-migrate-config: true` (opt-out by setting false).
Speed % now color-coded: red (<50%), gold (50-74%), yellow (75-99%), green (100-124%), aqua (125%+). Ships below 50% speed show a hint to add sails. Density line now combines the numeric value with a colored float status label (e.g., "1.33 (Floats well)") instead of separate lines. Removed surface offset from hover (redundant). Applied consistently across ship info hover, stats banner, and detection chat messages.
…er ref - Engine blocks now drop the custom ship engine item (with PDC + glint) instead of a vanilla blast furnace when broken - Add totalPositiveWeight field to ShipModel so assembled ships report correct positive weight instead of passing clamped maxHealth - Guard computeStat() against divide-by-zero when defaultRatio >= 1.0 - Use local plugin reference for engine PDC check in scanner instead of redundant global lookup
Rename totalPositiveWeight → mass (sum of max(0, weight) per block). This is the correct denominator for the power-to-mass ratio: it represents how much solid material sails need to push, excluding negative-weight floatation blocks. Fixes airships getting zero sail benefit — getSailRatio() previously returned 0 for negative totalWeight. Now uses mass, so airships with sails correctly get horizontal speed benefit. Also fixes: - engineBlockIndices changed from Set to List so iteration order matches engineLocalPositions (fixes smoke at wrong engine / IOOBE) - Lazy resolveWheelData() for chunk-recovered ships — looks up via ShipWheelManager.getWheelByShipUUID() on first access so fuel state is correctly loaded instead of assuming all engines fueled - Shift-click non-fuel into engine GUI now blocked - Dried kelp burn time 4001 → 4000 - YAML key renamed to "mass" with backwards-compat read of old "total_positive_weight" key
…er ref Engine PDC preserved on disassembly (is_engine flag in rawYaml, restored in placeBlocks). Ship info uses countFueledEngines() for ratio and shows fueled/unfueled engines separately. Smoke particles now spawn at engine shulker position instead of manual coordinate transform. Placed (unassembled) engine blocks open the custom fuel GUI instead of vanilla furnace UI. Shift-click non-fuel into engine GUI blocked. Smoke changed to CAMPFIRE_COSY_SMOKE for shorter duration. Known issues still unfixed: - countFueledEngines() doesn't know total engine set (uses computeIfAbsent side effect, phantom map entries); needs engine indices parameter - getShipInfo() uses stale lastDetected* fields for assembled ships instead of live ShipInstance data — mass can be 0/stale, effective power = mass - Engine fuel state and detection data are disconnected systems - Chat detection messages don't match lore format (missing mass, ratio) - Unassembled ships show engines at full potential points, not as unfueled
…back Root cause: assembleShip() never called setLastHealth() or setLastDetectedStats(), so the ship wheel menu fell into the "unassembled" display branch (lastMaxHealth=0) even for assembled ships. This showed all engines at full potential points instead of the fueled/unfueled breakdown. Fixes: - Set lastHealth + detection stats during assembly so menu has correct data immediately - getShipInfo() now reads live ShipInstance data (model + fuel state) for assembled ships instead of stale lastDetected* fields - countFueledEngines() takes engine block indices list so it checks ALL engines, not just those with map entries - getEngineFuelSlots() no longer uses computeIfAbsent (was creating phantom entries in the fuel map) - Physics fallback changed from engineCount (all fueled) to 0 when wheelData is null - tickEngineFuel skips engines with no fuel map entry - Engine display always shows fueled/unfueled breakdown regardless of assembled state - Engine GUI status shows Running/Ready/Idle based on burn ticks AND fuel items in slots
…tion deceleration scaling Previously wool power (3) and banner power (7) were hardcoded across multiple files. Fuel burn duration had no multiplier. Rotation deceleration used raw config values with no ratio-based scaling. Add wool-power, banner-power, fuel-burn-multiplier, floor-rotation-deceleration, and cap-rotation-deceleration to config.yml and ShipConfig. Consumers updated in subsequent commits.
…tation decel - Defer computeEffectiveStats() from constructor to after wheelData is linked. Previously the first stat computation always saw 0 fueled engines because wheelData wasn't set yet. Now recomputeStats() is called explicitly after assembly and lazily on recovery. - Guard minMovementThreshold so it only zeros speed when W/S aren't held — prevents low-ratio ships from being unable to move. - Return empty bucket when lava bucket fuel is consumed (vanilla parity). - Burn fuel on any movement input (A/D/Space/Sprint), not just W. - Apply fuel-burn-multiplier from config when consuming fuel items. - Scale rotation deceleration by power-to-mass ratio so heavy ships retain rotation momentum longer.
Engine fuel:
- Remap GUI fuel slots {1,2,3} → {0,1,2} for direct 1:1 mapping with
blast furnace container indices. Status slot moves from 5 → 4.
- Transfer pre-assembly fuel from blast furnace containers into
wheelData on assembly (was silently lost).
- Write wheelData fuel back to containers on disassembly (reverse gap).
- Clear stale fuel/burn entries on disassembly.
- Stop clearing entire blast furnace on save — targeted slot writes only.
- Crash-safe fuel deserialization with per-item try-catch.
- Add click-to-refresh on engine status item.
Stats display:
- Use weighted block count for density (matches physics, was using total
block count which included weightless blocks like trapdoors).
- Use config values instead of hardcoded 0.8 sail cap, 2 base power,
3 wool power, 7 banner power across all display paths.
- Standardize chat terminology: "power" → "pts".
Detection chat:
- Add chat output for assembled ship detection (was completely silent).
- Show live fuel state (fueled/unfueled engine breakdown) for assembled.
… dead data Engine GUI validation: - Block double-click collect and number-key hotbar swaps with non-fuel items. Add InventoryDragEvent handler to prevent drag-placing non-fuel. - Recompute ship stats on engine GUI close so fuel changes take effect immediately without requiring movement. Engine explosions: - Handle EntityExplodeEvent and BlockExplodeEvent for engine blocks — drop custom ship engine item instead of vanilla blast furnace. Null safety: - Guard null/invalid shulkers in camera distance update loop. ShipModel cleanup: - Remove dead engineLocalPositions field (populated but never read; smoke particles use collision shulker positions instead). - Accept woolPower/bannerPower as constructor params so sail power calculation uses config values. Old YAML with engine_local_positions key is silently ignored on load.
computeEffectiveStats() previously called ship.resolveWheelData(), which could trigger recomputeStats() → computeEffectiveStats() again when wheelData was first lazily resolved. The null guard prevented infinite recursion but the double computation was wasteful and fragile. Read ship.wheelData directly instead — all callers of recomputeStats() (assembly, recovery, GUI close) guarantee wheelData is set beforehand.
…orms (1/3) Root cause: the entity tracker sends vehicle rotation at byte precision (~1.4° quantization) every 3 ticks, conflicting with float-precision position sync packets sent every tick — creating periodic jitter. Fix: freeze the vehicle's yaw at spawnYaw and track rotation internally via physics.currentYaw. All visual rotation is applied through display entity transformation matrices, bypassing the entity tracker entirely. Changes: - ShipPhysics: add currentYaw field, rotation updates it instead of teleporting the vehicle, forward direction and snap methods use it - ShipInstance: all vehicle.getYaw() → physics.currentYaw, removed version-specific display rotation branch (always apply delta rotation via transformation), added idle yaw sync and setInterpolationDelay(0) - ShipCollision: collision forward direction uses physics.currentYaw Known issues to fix in follow-up commits: - idle yaw sync causes double rotation when ship stops (display transformation not updated on idle tick but vehicle yaw jumps) - setInterpolationDelay(0) causes item display Y jitter (interpolation duration > update interval creates cascading positional lag) - DisplayShip dismount velocity uses frozen vehicle yaw (wrong direction) - ShipPersistence saves frozen vehicle yaw (loses rotation on chunk save) - currentYaw can grow unbounded without normalization
Issues observed during in-game testing of the internal yaw tracking: - idle yaw sync caused double rotation on stop: when the ship went idle, the vehicle yaw was teleported from spawnYaw to currentYaw, but the display transformation (still carrying deltaYaw) was not updated on idle ticks — resulting in visual = inheritedYaw + deltaYaw = 2x rotation. Fix: remove idle yaw sync entirely, vehicle yaw stays frozen forever. - setInterpolationDelay(0) caused item display Y jitter: with interpolation_duration=2 but updates every 1 tick, each interpolation was interrupted before completion, creating cascading positional lag visible as massive downward jitter on item displays. Fix: remove setInterpolationDelay(0), let transforms snap at 20 Hz (still 3x smoother than the original 6.67 Hz byte-precision tracker). - dismount velocity used frozen vehicle yaw: players ejected from a rotated ship would fly in the original spawn direction. Fix: use physics.currentYaw in DisplayShip dismount calculation. - persistence saved frozen vehicle yaw: chunk unload would lose the ship's actual rotation. Fix: save physics.currentYaw in ShipPersistence.fromInstance(). - snapToFineGrid changed vehicle yaw: driver exit would unfreeze the vehicle yaw, re-introducing entity tracker byte-precision packets. Fix: only snap position, keep vehicle yaw frozen. Remaining: currentYaw can grow unbounded without normalization.
Custom ships lost their rotation on chunk reload because recoverEntities() set spawnYaw to model.initialRotation.x (assembly yaw), but the idle sync had already reset spawnYaw to currentYaw. This mismatch created a non-zero deltaYaw on recovery, causing double rotation (inherited vehicle yaw + transformation delta both contributing rotation). Fix: always set spawnYaw = vehicle.getYaw() on recovery (for all ship types), matching the idle sync's invariant that spawnYaw == currentYaw. deltaYaw is 0 on recovery, display shows R(initial), and the vehicle's inherited yaw provides the actual rotation. Also: - Extract updateDisplayTransforms() method from tick() so the idle yaw sync can call it after resetting spawnYaw (ensures display matches the synced state immediately, preventing one-tick visual glitch) - Add idle yaw sync: when ship stops, sync vehicle yaw to currentYaw for NBT persistence, reset spawnYaw to currentYaw, update displays - Normalize currentYaw to [0,360) after each rotation update - Use normalizeAngle() for position sync yaw delta to handle wrapping - Normalize spawnYaw/currentYaw at all init sites
…oGrid (4/3) The idle yaw sync teleported the vehicle on the first idle tick to update its NBT yaw for chunk persistence. This caused a 1-frame visual pop: the ENTITY_TELEPORT packet (changing vehicle yaw) and the display entity metadata update (resetting transform delta to 0) arrive at the client as separate packets, momentarily showing double-rotation. Root fix: save physics.currentYaw in per-world YAML metadata (ShipWorldData) so chunk recovery no longer depends on vehicle entity NBT yaw. The idle sync teleport is then unnecessary and removed entirely. Vehicle yaw now stays frozen at spawnYaw for the entity's entire lifetime. Metadata uses config.contains() to distinguish legacy files (no current_yaw key → Float.NaN sentinel → fall back to vehicle NBT) from files where the ship genuinely faces yaw=0 (north). Also fixes alignToGrid(): the vehicle teleport was passing snappedYaw, unfreezing the vehicle yaw. Now preserves loc.getYaw() (frozen) and resets spawnYaw + refreshes display transforms in the ShipInstance wrapper. ShipWheelManager.disassembleShip() reads physics.currentYaw instead of the frozen vehicle yaw for block placement rotation.
BACKGROUND The rotation system (82bda96) freezes the vehicle entity's yaw at spawnYaw for its entire lifetime. All visual rotation is applied through display entity transformation matrices using: deltaYaw = physics.currentYaw - spawnYaw On 1.21.9+, Minecraft clients inherit the parent vehicle's yaw when rendering display entity passengers. This means the client renders: visual rotation = vehicle.getYaw() + deltaYaw + initialModelRotation = spawnYaw + (currentYaw - spawnYaw) + initial = currentYaw + initial ✓ This is only correct when the invariant holds: spawnYaw == vehicle.getYaw() If spawnYaw is reset to a value that differs from vehicle.getYaw(), the client's inherited vehicle rotation no longer cancels out and the ship appears at the wrong angle. WHAT 1648bb5 BROKE 1648bb5 replaced the idle yaw sync with per-world metadata persistence (saving physics.currentYaw to ShipWorldData YAML). The goal was correct — the idle sync caused a 1-frame double-rotation pop because the ENTITY_TELEPORT packet (updating vehicle yaw) and the display metadata packet (resetting deltaYaw to 0) arrived at the client in separate frames. Metadata persistence avoids this by never changing vehicle yaw at all. However, 1648bb5 introduced two bugs that break the invariant: BUG 1 — alignToGrid() froze vehicle yaw while resetting spawnYaw alignToGrid() in ShipPhysics was changed to pass loc.getYaw() (the frozen vehicle yaw) in the teleport Location instead of snappedYaw. The ShipInstance wrapper then does: spawnYaw = physics.currentYaw; // = snappedYaw This left vehicle.getYaw() at the old frozen value while spawnYaw was updated to snappedYaw — breaking the invariant. After align (before fix): vehicle.getYaw() = oldFrozenYaw (e.g. 47°) spawnYaw = 90° (reset in wrapper) deltaYaw = 0° (currentYaw - spawnYaw) client renders = 47° + 0° = 47° ← wrong In-game effect: after using the align command, the ship's display blocks would visually snap to their spawn orientation (the old frozen yaw), while the collision boxes remained at the correct snapped angle. Invisible walls next to visible blocks. BUG 2 — chunk recovery set spawnYaw from metadata instead of vehicle NBT recoverEntities() was changed to set BOTH spawnYaw and currentYaw from the metadata yaw: spawnYaw = metadataYaw (e.g. 90°) currentYaw = metadataYaw (e.g. 90°) deltaYaw = 0° But the vehicle entity loads from chunk NBT with its original frozen yaw (e.g. 0°), which the client sees. So: vehicle.getYaw() = 0° (from NBT — was never updated) spawnYaw = 90° (from metadata) deltaYaw = 0° client renders = 0° + 0° = 0° ← should be 90° In-game effect: every ship that had been rotated from its spawn angle would appear at the wrong rotation after chunk unload/reload. Walking away from a ship and returning would show it "unrotated" back to its spawn angle, while collisions remained at the correct rotated position. FIXES Fix 1 — revert alignToGrid() to update vehicle yaw (ShipPhysics.java) Restore snappedYaw in the teleport Location for alignToGrid(). The ship is stationary after align, so there is no active position-sync packet to conflict with the rotation packet — no byte-precision jitter. Cardinal angles (90° multiples) also map exactly to byte encoding (90/360 x 256 = 64), so there is zero quantization error. After align with fix: vehicle.getYaw() = 90° (updated by teleport) spawnYaw = 90° (reset in wrapper) currentYaw = 90° deltaYaw = 0° client renders = 90° + 0° = 90° ✓ Invariant maintained. The ShipWheelManager change reading ship.physics.currentYaw for block placement remains correct (same value as vehicle.getYaw() post-align, but more explicit). Fix 2 — recovery sets spawnYaw from vehicle NBT, currentYaw from metadata (ShipInstance.java recoverEntities) spawnYaw must equal vehicle.getYaw() (what the client inherits from NBT). The metadata yaw goes into currentYaw, making deltaYaw provide the compensation: spawnYaw = normalizeYaw(vehicle.getYaw()) // e.g. 0° (NBT) currentYaw = normalizeYaw(metadataYaw) // e.g. 90° deltaYaw = 90° - 0° = 90° client = 0° + 90° = 90° ✓ Legacy metadata without current_yaw falls back to spawnYaw, giving deltaYaw = 0 — identical to pre-metadata behaviour. Fix 3 — replace R_initial-only init display loop with updateDisplayTransforms() (ShipInstance.java recoverEntities) The previous init loop applied only R_initial (no delta). With fix 2 making spawnYaw != currentYaw, this would cause a 1-tick flash at the wrong angle before the first tick applied the correct transform. Replacing the 18-line manual loop with updateDisplayTransforms() applies the full deltaYaw immediately on recovery, eliminating the flash. The call is safe at this point: vehicle, displays, spawnYaw, currentYaw, cachedR_initial, and all work matrices are fully initialized. WHAT REMAINS CORRECT FROM 1648bb5 - ShipWorldData saving physics.currentYaw and loading via config.contains() (correct sentinel, handles yaw=0 case) - Idle sync removal (safe — spawnYaw is never reset during normal ticking, so the invariant holds without it) - alignToGrid() wrapper (spawnYaw reset + updateDisplayTransforms) - ShipWheelManager reading physics.currentYaw for block placement
Adds `custom-ships.destruction-mode` to config.yml with two values: - `disassemble` (default): current behavior — blocks placed back into the world, wheel block broken and dropped, explosions spawned. - `destroy`: ship is permanently lost. Stored inventory contents and engine fuel drop as loose items at the ship's location; blocks and the wheel item are not recovered. Explosions still spawn. Implementation: - ShipConfig: adds `destroyOnDeath` boolean field loaded from `custom-ships.destruction-mode` (true iff value == "destroy"). - ShipInstance.destroyAndDropItem(): when destroyOnDeath is set, drops storages and fuel then calls destroyWithCleanup() instead of routing through disassembleShip(). Prefab ships are unaffected (branch is inside the "custom".equals(shipType) guard). - ConfigValidator.migrateConfig() picks up the new key automatically via its full-key scan of the bundled config. Three bugs are fixed in the following commit (found during review): - destroyWithCleanup() called without null-guarding getDisplayShip() - wheel block left in-world and placedWheels map not cleaned up on destroy, causing the wheel to reappear after server restart - lead items silently lost when ship shulkers are removed; players lose leads with no feedback or item drop
The ship stats system (5da91fd) introduced power-to-mass ratio scaling. Custom test ships had no wool or banners beyond one existing banner, giving them a ratio of ~0.19 (power 9 / mass 47). At this ratio ships crawl at floor speed, and the test's forward+backward control sequence results in near-zero net displacement (total=1.37, need >=2). Add 3 banners and 1 wool block to the surface layer of both custom_ship and custom_airship test builds. This raises custom_ship's ratio to ~0.53 (power 26 / mass 49), well above the floor and close enough to the default 0.7 to move at reasonable speed through the test sequence.
…tity scope PlayerInput: handle dismount explicitly via dismountPlayer() instead of relying on PlayerToggleSneakEvent (not guaranteed in vehicles). Expand registration details and threading analysis. Tile entities: scope down to chiseled bookshelves + signs only. Correct API usage from code review (getSnapshotInventory, serializeInventory signature). Note existing container item duplication bug to fix.
On Paper 1.21.2+, use the native PlayerInputEvent API for ship steering instead of ProtocolLib packet interception. Falls back to ProtocolLib on older servers. This also fixes the 1.21.3-1.21.8 gap where ProtocolLib reflection on the Input record failed, and eliminates a latent JMM race condition (ProtocolLib writes input booleans from the netty thread while physics reads them on the main thread — both are plain non-volatile fields).
Shelves (1.21.9+, 12 variants) and chiseled bookshelves now allowed in custom ships. Both use TileStateInventoryHolder (not Container), so a single instanceof check handles serialization and restoration. Inventory cleared before block removal to prevent item duplication. Shelf items show as empty BlockDisplay during flight; items restored on disassembly. Sign text (front/back lines, dye color, glow, waxed state) now serialized on assembly and restored on disassembly. Text still not visible on BlockDisplay during flight (Minecraft limitation).
…elf rotation Destroy-on-death path now drops items from shelves and chiseled bookshelves. These blocks use TileStateInventoryHolder (not Container) so their items live in rawYaml["container_items"] rather than the storages map. The new loop iterates sourceModel.parts, guarded by part.storage == null to avoid double-dropping items already handled by the Container/storages path. Also adds display_rotation: true to chiseled_bookshelf and *_shelf entries in blocks.yml — these are directional blocks whose facing is ignored by BlockDisplay entities, so the ship system needs to apply manual Y-axis rotation transforms (same mechanism used for chests and barrels).
Container inventories were serialized into rawYaml but never cleared from the world block. When removeBlocks() later calls setType(AIR), the block drops its items — duplicating items already captured in the serialized data. Fix: clear the snapshot inventory and call update() after serialization to push the empty state to the world block before removal. Applied to both the Container path (chests, furnaces, hoppers) and TileStateInventoryHolder path (shelves, chiseled bookshelves). Also corrects the TileStateInventoryHolder clearing to use getSnapshotInventory().clear() + update() instead of getInventory().clear() — the snapshot-then-update pattern is the correct Bukkit API usage and matches the Container path.
…eview Mark chiseled bookshelves, shelves, and signs as done in the tile entity feature item. Add three code quality items identified during review: Container/TileStateInventoryHolder double-match in BlockStructureScanner (with brewing stand edge case note), uncached block.getState() calls in serialization loop, and unused plugin param in PaperInputListener.
Airships need more time to decelerate vertically after chunk recovery, especially with PaperInputListener (main-thread input vs ProtocolLib's netty thread). Settle wait 3s→5s, post-dismount wait 1s→2s.
… settle setControlState only sends a packet when state changes — if already false, no packet fires and the plugin retains stale input from the last steerShip tick. Send explicit all-false player_input packet to guarantee the plugin clears input state. Also increase settle wait 3s→10s and post-dismount wait 1s→5s for slow CI hardware.
Bot was getting kicked with "Failed to decode packet 'serverbound/minecraft:accept_teleportation'" on 1.21.11 servers. The installed minecraft-data (3.102.3) only had protocol data up to 1.21.8 — the teleportation confirmation packet format changed in newer versions and the bot was sending a malformed packet. mineflayer 4.37.1 pulls minecraft-protocol 1.66.2 and minecraft-data 3.110.2, which includes 1.21.9 and 1.21.11 protocol definitions.
Root cause: mineflayer's bot.moveVehicle() sends player_input with
forward/backward/left/right but omits jump. bot.setControlState('jump')
only sets a local physics flag (jumpQueued) without sending any packet.
On 1.21.2+ where PaperInputListener processes PlayerInputEvent, the
server never receives jump input — airships can't ascend.
The steerShip helper had a workaround for 1.21.8 specifically
(useRawPlayerInput = bot.version === '1.21.8') that sends raw
player_input packets with all fields. Extend this to all 1.21.2+
versions where the new player_input packet format is used. Also add
sprint field for completeness (used for airship descent).
This fixes smallairship, custom_airship, and chunk_persistence_airship
CI failures on 1.21.2+ servers.
With the jump input fix (13e84cb), airships now properly receive jump input on 1.21.2+ and accelerate much more than before. The 2s climb duration built up too much vertical velocity for the 10s settle period on slow CI hardware. Reduce to 500ms — the test only needs to verify the ship moved, not that it moved far.
mineflayer's bot.entity.position doesn't update while riding a vehicle. After dismount, the plugin teleports the player to a safe position near the ship, but mineflayer may not process the teleport packet in time. For airships that climbed vertically, movedPos was stuck at the pre-mount Y=105 while the ship was at Y≈170 — the test then teleported back to ground level after the chunk cycle and measured 66 blocks to the ship. Send /tp @s ~ ~ ~ after dismount to force a server→client position packet so mineflayer syncs before recording movedPos.
Root cause: for airships, the plugin teleports the dismounting player to ground level (Y≈105) while the ship stays at altitude (Y≈160+). The test used bot.entity.position as the reference point, then compared shulker positions against it after a chunk cycle — measuring 57-66 blocks of "drift" that was actually just the vertical gap between ground level and the airship. Fix: compute average shulker position before the chunk cycle and use that as the reference point. Teleport back to the ship's position (not the bot's ground-level dismount position) for the chunk cycle. Also add diagnostic logging: shulker positions with full XYZ at three points (right after dismount, 1s later, after teleport back) to detect any actual drift during chunk recovery.
…ulker positions Shulkers are passengers of carrier ArmorStands. The MC server does not send position update packets for passenger entities — the vanilla client calculates their render position from the vehicle. Mineflayer does not implement this client-side positioning, so bot.entities[id].position for shulkers stays frozen at assembly-time coordinates after ship movement. The chunk_persistence test steers the ship ~36 blocks forward, dismounts, reads shulker positions (stale, still at assembly coords Z≈-2), cycles chunks, then reads shulker positions again (fresh from spawn packets, true position Z≈-35). The 36-block "drift" was never real — it was the distance traveled during steering, measured correctly post-cycle but compared against stale pre-cycle data. Confirmed from CI logs (run 27178642083, paper 1.21.4): Pre-cycle shulker avg (stale): Z = -1.7 (assembly position) Bot dismount pos (server truth): Z = -35.6 (36.2 blocks from start) Post-cycle shulkers (fresh): Z = -34.5 to -36.5 Bot position ≈ post-cycle shulkers. Pre-cycle was stale. The root cause was commit d3c190b which switched the pre-cycle reference from bot.entity.position (server truth via plugin safe-teleport + /tp sync) to avgShulkerPos (stale passenger positions), to work around the airship Y-offset on dismount (bot teleported to ground, ship at altitude). Fix: add getShipEntityPos() which reads shulker.vehicle?.position (the carrier ArmorStand, a standalone entity with normal position tracking) with fallback to shulker.position (correct post-reload when spawn packets provide fresh positions). This works for both water ships and airships since carrier positions include vertical movement — no Y-offset issue.
On 1.21.3+, bot.dismount() sends player_input { jump: true } which
PaperInputListener interprets as climb input, not dismount. This leaves
isSpacePressed=true on the driverless ship, causing indefinite vertical
acceleration (~39 blocks of drift in CI).
Test fix (helpers.js): on 1.21.3+, try raw player_input { shift: true }
first (correct PaperInputListener dismount), demote bot.dismount() to
fallback, remove the { jump: true } fallback entirely. Send all-false
player_input after every successful dismount to clear stale input state.
Plugin fix (ShipPhysics.java): gate isSpacePressed/isSprintPressed on
hasDriver in applyAirshipVerticalPhysics() so a driverless airship never
responds to stale input flags regardless of how they were set.
Also: verify entity counts by type (armor_stand, shulker, block_display,
item_display) across chunk cycles, remove 1.21.4 from VERSION_SKIPS.
…n check
On 1.21.1, the player_input packet doesn't exist — protodef silently
maps the unknown name to varint 0x00 (teleport_confirm), so the server
receives a zero-payload teleport confirm and kicks the bot. Guard the
write with supportFeature('newPlayerInputPacket') to match the existing
clearShipInput pattern in helpers.js.
26.2 ("Chaos Cubed") released 2026-06-16. Paper (26.2-24) and Purpur
(build 2595) both have builds available. No conditional changes needed:
the workflow already keys Java 25 selection and bot-test skipping off
startsWith(matrix.minecraft, '26.').
In biomes where drowned spawn heavily, the special ship-wheel-dropping drowned appeared in overwhelming numbers. Lower the default spawn-chance from 0.05 to 0.02, keeping the config default and the code fallback in sync. The feature can still be disabled or tuned via config.
…ization - TODO-land-vehicles.md: design for Land Vehicle Wheel and horse-style step-up movement, reusing the existing custom-ship type plumbing (#10) - TODO-profiling.md: nanoTime instrumentation + /blockships perf command to measure tick budget, split by terrain vs ship-to-ship collision - TODO-skip-interior.md: skip spawning collider pairs for fully-enclosed interior blocks to cut per-tick entity overhead - TODO-ci.md: root-cause writeup for the chunk_persistence test failures (stale mineflayer positions, a test bug — not ship drift) - TODO-playerinput.md: add 26.2 to the documented CI version matrix
- cross-reference issues v0.0.16 resolves or partially addresses: #28 (optional ProtocolLib), #23 (shelves/bookshelves/signs, partial), #7 (chunk-reload rotation snap, partial), #29 (rarer drowned captains) - note the Captain's Manual is now a craftable item - add a "Known Issues / Not Yet Addressed" section covering #20 (wall heads), #23 (pots/other tile entities), #24 (partial destruction), #7 (legacy collider desync), and #28 (pending reporter confirmation) - TODO.md: add deck-physics note (carry standing players with moving ships)
Bukkit.getBukkitVersion() on Paper 26.2 returns "26.2.build.24-alpha", a new API-version format. The old parser split on "-" then "." and called Integer.parseInt() on every resulting segment, so parts[2] = "build" threw NumberFormatException and the parser fell back to the hardcoded 1.21.11. This surfaced in the 26.2 CI startup test as: [ServerVersion] Could not parse version '26.2.build.24-alpha': For input string: "build" - defaulting to 1.21.11 The fallback is logged at SEVERE (-> [ERROR]: [BlockShips] ...), which the CI error grep treats as a failure, so the 26.2 job went red. (26.1.2 used the older "26.1.2-R0.1-SNAPSHOT" format and parsed fine.) Parse only the leading numeric dot-separated segments and stop at the first non-numeric one, treating any remaining components as 0. "26.2.build.24-alpha" now yields 26.2.0; existing formats are unchanged (1.21.11-R0.1-SNAPSHOT -> 1.21.11, 1.21 -> 1.21.0). A version with no leading numeric segment still throws into the unchanged 1.21.11 fallback. The 1.21.11 fallback was functionally harmless today (all isAtLeast() gates check thresholds <= 1.21.9, true for both 26.2 and the fallback), but the parser now reports the real version and no longer logs a spurious error. Verified the parse logic against 26.2.build.24-alpha, 26.2.0, 26.1.2-R0.1-SNAPSHOT, 1.21.11/1.21.1/1.21, and an unparseable string.
PaperInputListener (added in e15f794) makes ProtocolLib unnecessary on 1.21.2+. Replace the glob copy of all cached plugin jars with explicit per-plugin copies, guarded by ifeq on MINECRAFT_VERSION for ProtocolLib. Also pass MINECRAFT_VERSION to the test-server-setup CI step -- without this, Make's ifeq always sees the default (1.21.11) and ProtocolLib is never copied, breaking the 1.21.1 matrix entry where it's still required.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.